package com.inwavetech.os.sync;

import android.util.Log;

import com.inwavetech.os.entity.Os;
import com.inwavetech.os.entity.Product;
import com.inwavetech.os.entity.User;
import com.inwavetech.os.sync.jsonparser.InvalidEntityJsonException;
import com.inwavetech.os.sync.jsonparser.OsJsonParser;
import com.inwavetech.os.sync.jsonparser.ProductJsonParser;
import com.inwavetech.os.util.DaoHelper;
import com.inwavetech.os.util.SessionManager;
import com.inwavetech.os.util.SimpleResponse;
import com.inwavetech.os.util.SyncDateFormat;
import com.inwavetech.os.util.request.Get;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.misc.TransactionManager;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;

/**
 * Get new and updated os from the server
 */
public class OsUpdater {
    private final SessionManager sessionManager;
    private final Get request;
    private final SyncDateFormat syncDateFormat;
    private final DaoHelper daoHelper;
    private final OsJsonParser osJsonParser;
    private final ProductJsonParser productJsonParser;
    private final Dao<Os, ?> osDao;
    private final Dao<Product, ?> productDao;
    private final Inserter inserter;
    private final Updater updater;
    private final Dao<User, ?> userDao;
    private String baseUrl;

    public OsUpdater(
            DaoHelper daoHelper,
            SessionManager sessionManager,
            Get request,
            SyncDateFormat syncDateFormat,
            OsJsonParser osJsonParser,
            ProductJsonParser productJsonParser,
            String baseUrl) {
        this.daoHelper = daoHelper;
        this.sessionManager = sessionManager;
        this.request = request;
        this.baseUrl = baseUrl;
        this.syncDateFormat = syncDateFormat;
        this.osJsonParser = osJsonParser;
        this.productJsonParser = productJsonParser;

        osDao = daoHelper.getDao(Os.class);
        productDao = daoHelper.getDao(Product.class);
        userDao = daoHelper.getDao(User.class);

        inserter = new Inserter(osJsonParser, productJsonParser, daoHelper);
        updater = new Updater(osJsonParser, productJsonParser, daoHelper);
    }

    public void execute() throws IOException, SessionManager.SessionManagerException, SQLException, JSONException, BadFormatResponse {
        User user = sessionManager.getUser();

        if (user == null) {
            return; // no user? bb sync
        }

        String urlDatePart = "";
        if (user.last_updated_at_data != null) {
            urlDatePart = "&updated_at=" + syncDateFormat.format(user.last_updated_at_data);
        }

        int page = 0;
        while (true) {
            JSONArray oscol;
            String url = String.format(baseUrl, ++page); // page
            SimpleResponse response = request.execute(url + urlDatePart);

            try {
                oscol = new JSONArray(response.body);
            } catch (JSONException e) {
                throw new BadFormatResponse("response: " + response.body);
            }

            if (oscol.length() == 0) {
                break;
            }

            ArrayList<String> receivedIds = new ArrayList<String>();
            for (int i = 0; i < oscol.length(); i++) {
                receivedIds.add(oscol.getJSONObject(i).getString("id"));
            }

            List<Os> inDbOsColl = osDao.queryBuilder().where().in("original_id", receivedIds).query();
            HashMap<String, Os> mappedInDbOs = new HashMap<String, Os>();
            for (Os os: inDbOsColl) {
                mappedInDbOs.put(os.original_id, os);
            }

            Os os;
            boolean success;
            Date lastUpdatedAt = null;
            for (int i = 0; i < oscol.length(); i++) {
                JSONObject osJson = oscol.getJSONObject(i);

                String id = osJson.getString("id");
                if (mappedInDbOs.containsKey(id)) {
                    os = mappedInDbOs.get(id);
                    success = updater.execute(osJson, os);
                } else {
                    os = new Os();
                    os.user = user;

                    success = inserter.execute(osJson, os);
                }

                if (success) {
                    lastUpdatedAt = os.updated_at;
                }
            }

            if (lastUpdatedAt != null) {
                user.last_updated_at_data = lastUpdatedAt;
                userDao.update(user);
            }
        }
    }

    static abstract class DbUpdater {
        private final ProductJsonParser productJsonParser;
        private final DaoHelper daoHelper;
        protected final Dao<Os, ?> osDao;
        protected final Dao<Product, ?> productDao;
        protected final OsJsonParser osJsonParser;

        public DbUpdater(OsJsonParser osJsonParser, ProductJsonParser productJsonParser, DaoHelper daoHelper) {
            this.osJsonParser = osJsonParser;
            this.productJsonParser = productJsonParser;
            this.daoHelper = daoHelper;

            osDao = daoHelper.getDao(Os.class);
            productDao = daoHelper.getDao(Product.class);
        }

        public boolean execute(final JSONObject osJson, final Os os) throws SQLException {
            return TransactionManager.callInTransaction(daoHelper.getConnectionSource(), new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    try {
                        doExecute(osJson, os);
                    } catch (InvalidEntityJsonException e) {
                        Log.e(OsUpdater.class.getName(), String.format(
                                "the os '%s' could not be imported due to the following fields '%s'",
                                osJson.get("id"),
                                e.getInvalidKeys()
                        ));
                        new SQLException("stop the transaction!");

                        return false;
                    }

                    return true;
                }
            });
        }

        private void doExecute(JSONObject osJson, Os os) throws JSONException, InvalidEntityJsonException, SQLException {
            if (!shouldPersist(osJson, os)) {
                return;
            }

            osJsonParser.parse(osJson, os);

            persistOs(os);

            JSONArray productsJson = osJson.getJSONArray("products");
            for (int f=0; f<productsJson.length(); f++) {
                Product product = new Product();
                product.os = os;

                JSONObject productJson = productsJson.getJSONObject(f);
                productJsonParser.parse(productJson, product);

                persistProduct(product);
            }
        }

        protected abstract boolean shouldPersist(JSONObject osJson, Os os) throws JSONException;

        abstract protected void persistProduct(Product product) throws SQLException;

        abstract protected void persistOs(Os os) throws SQLException;
    }

    static class Inserter extends DbUpdater {

        public Inserter(OsJsonParser osJsonParser, ProductJsonParser productJsonParser, DaoHelper daoHelper) {
            super(osJsonParser, productJsonParser, daoHelper);
        }

        @Override
        protected boolean shouldPersist(JSONObject osJson, Os os) throws JSONException {
            // only active os should be created
            return osJsonParser.validateAndClean(osJson, "status_id").equals(Os.Status.ACTIVE);
        }

        @Override
        protected void persistProduct(Product product) throws SQLException {
            productDao.create(product);
        }

        @Override
        protected void persistOs(Os os) throws SQLException {
            osDao.create(os);
        }

    }

    static class Updater extends DbUpdater {

        public Updater(OsJsonParser osJsonParser, ProductJsonParser productJsonParser, DaoHelper daoHelper) {
            super(osJsonParser, productJsonParser, daoHelper);
        }

        @Override
        protected boolean shouldPersist(JSONObject osJson, Os os) throws JSONException {
            return ((Date) osJsonParser.validateAndClean(osJson, "updated_at")).after(os.updated_at);
        }

        @Override
        protected void persistProduct(Product product) throws SQLException {
            productDao.create(product);
        }

        @Override
        protected void persistOs(Os os) throws SQLException {
            osDao.update(os);
            productDao.delete(os.products);
        }

    }

    static class BadFormatResponse extends Exception {
        public BadFormatResponse(String detailMessage) {
            super(detailMessage);
        }
    }
}
